/*
 *  Copyright 2010 Wesley Gooch, Ryan Nix, Cody Wenzel.
 *  Montana State University - Spring Semester 2010
 *  Computer Science 450 - Compilers
 *  Project: Micro-Pascal Compiler
 */
package msu.compiler.scanner;

import java.io.IOException;
import java.io.PushbackReader;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 
 * @author Wesley Gooch
 */
public class Tokenizer {

    private PushbackReader inputReader;
    private byte currentTokenType = Token.MP_ERROR;
    private TokenLookup lookupList;
    private int currentChar;
    private int line = 1;
    private int column = 0;
    private StringBuilder lexemeBuffer = new StringBuilder();
    private int lexemeLine = 1;
    private int lexemeColumn = 0;
    private int markIndex = 1;
    private int[] markPointer = new int[2];
    private boolean atEOF = false; // flag to indicate if the EOF has been reached
    private boolean lookingAhead = false;

    public Tokenizer(PushbackReader input) {
        inputReader = input;
        lookupList = new TokenLookup();
    }

    /**
     * Returns the next non-whitespace character in the input.
     * 
     * @return the int value of the char read or -1 if eof or 0 if error.
     */
    public int readNextChar() {
        // reset before we begin.
        lookingAhead = false;
        
        skipWhiteSpace();
        if ((char) currentChar == '{') consumeComment();
        
        if (!atEOF) { 
            lexemeBuffer.append((char) currentChar);
            setMark(); // Initialize marks so we backup if needed.
            // Initialize the start of this lexeme.
            lexemeLine = line;
            lexemeColumn = column;
        }
        

        return currentChar;
    }

    // ------------------------------------------------------------------
    // FSA Interface Methods
    // ------------------------------------------------------------------
    public char getCurrentChar() {
        return lexemeBuffer.charAt(lexemeBuffer.length() - 1);
    }

    public void peekAhead() {
        lookingAhead = true;
        lexemeBuffer.append((char) getNextChar());
    }

    /**
     * Mark a fallback point on the lexemeBuffer.
     */
    public void setMark() {
        markIndex = lexemeBuffer.length();
        markPointer[0] = line;
        markPointer[1] = column;
    }

    public void revertToMark() {
        int backupBy = lexemeBuffer.length() - markIndex;
        if (backupBy > 0) {
            line = markPointer[0];
            column = markPointer[1];
            if (column <= 0) column = 0;
            for (int i = 0; i < backupBy; i++) {
                try {
                    char ch = lexemeBuffer.charAt(lexemeBuffer.length() - 1);
                    inputReader.unread(ch);
                    lexemeBuffer.deleteCharAt(lexemeBuffer.length() - 1);
                } catch (IOException ex) {
                    Logger.getLogger(Tokenizer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    /**
     * Compares the current lexemeBuffere against reserved words. If the lexeme
     * is a reserved word then the currentTokenType is set accordingly.
     */
    public void checkReservedWords() {
        byte resolvedType = lookupList.resolve(lexemeBuffer.toString());
        if (resolvedType >= 0) {
            setCurrentTokenType(resolvedType);
        }
    }

    public void setCurrentTokenType(byte currentTokenType) {
        this.currentTokenType = currentTokenType;
    }

    public boolean isAtEOF() {
        return atEOF;
    }

    // ------------------------------------------------------------------
    // Current Token Setting & Capturing
    // ------------------------------------------------------------------
    /**
     * Captures the current token. If there is an error message present then
     * an error token is returned otherwise just a normal token.
     *
     * @return The current token object.
     */
    public Token captureToken() {
        Token token;
        switch (currentTokenType) {
            case Token.MP_RUN_COMMENT:
                token = new Token(currentTokenType, "Runaway Comment", lexemeLine, lexemeColumn, "Run-on Comment Error!");
                captureLexeme();
                break;
            case Token.MP_RUN_STRING:
                token = new Token(currentTokenType, "Runaway Comment", lexemeLine, lexemeColumn, "Run-on String Literal Error!");
                captureLexeme();
                break;
            case Token.MP_ERROR:
                token = new Token(currentTokenType, captureLexeme(), lexemeLine, lexemeColumn, "Token does not belong to the language.");
                break;
            case Token.MP_EOF:
                token = new Token(Token.MP_EOF, "EOF", line, column);
                break;
            default:
                token = new Token(currentTokenType, captureLexeme(), lexemeLine, lexemeColumn);
                break;
        }
        if(atEOF) {
            column++;
            setCurrentTokenType(Token.MP_EOF);
        }
        return token;
    }

    private String captureLexeme() {
        String lexeme = lexemeBuffer.toString();
        lexemeBuffer = new StringBuilder(); // reset the lexeme.

        return lexeme;
    }

    // ------------------------------------------------------------------
    // Pointer and Whitespace Management Private Methods
    // ------------------------------------------------------------------
    /**
     * Sets the currentChar to the next char and moves the pointer. Errors
     * are handled if encountered. This method does not try to interpret
     * the next char... just moves to it.
     *
     * @return returns the integer value of the next char.
     */
    private int getNextChar() {
        try {
            currentChar = inputReader.read();
            if (currentChar == -1) {
                atEOF = true;
                if(!lookingAhead) setCurrentTokenType(Token.MP_EOF);
            } else {
                movePointer();
            }
            return currentChar;

        } catch (IOException ex) {
            Logger.getLogger(Tokenizer.class.getName()).log(Level.SEVERE, null, ex);
            currentChar = 0;
            return 0;
        }
    }

    /**
     * Moves the pointer to the position of the currentChar.
     */
    private void movePointer() {
        if (isEndOfLine(currentChar)) {
            line++;
            column = 0;
        } else {
            column++;
        }
    }

    /**
     * Grabs the next character and checks to see if it's whitespace. If
     * it is, then keep iterating over the input until a non-whitespace 
     * character is reached or the EOF/Error occurs.
     */
    private void skipWhiteSpace() {
        while (Character.isWhitespace((char) getNextChar())) {
            // Do nothing with the white space. Just calling getNextChar
            // in the while loop comparator is enough to move past all
            // white space.
        }
    }

    /**
     * Consumes the all characters including and between {...}
     * 
     * @pre     The currentChar must be a { character.
     * @post    The currentChar must be the char directly after a } character
     *          if not, then generate a commentRun error Token.
     */
    private void consumeComment() {
        if (currentChar == '{') {
            // Initialize the start of this lexeme.
            lexemeLine = line;
            lexemeColumn = column;
            while (getNextChar() != '}') {
                // Do nothing with comment chars but check for an
                // EOF incase this is a runaway comment.
                if (atEOF) {
                    setCurrentTokenType(Token.MP_RUN_COMMENT);
                    break;
                }
            }
            
            // Move to the next non-whitespace after the comment
            if (!atEOF) {
                skipWhiteSpace();
                if ((char) currentChar == '{') consumeComment();
            }
        }
    }

    // ------------------------------------------------------------------
    // Static Character Testing Methods
    // ------------------------------------------------------------------
    public static boolean isLetter(char ch) {
        int intValue = (int) ch;
        // Lowercase letter char-to-int values will be between 97-122
        // Uppercase letter char-to-int values will be between 65-90
        return ((intValue >= 97 && intValue <= 122) || (intValue >= 65 && intValue <= 90));
    }

    public static boolean isDigit(char ch) {
        int intValue = (int) ch;
        // char numbers 0-9 are represented by ints 48-57
        return (intValue >= 48 && intValue <= 57);
    }

    public static boolean isLetterOrDigit(char ch) {
        return (isDigit(ch) || isLetter(ch));
    }

    public static boolean isEndOfLine(int ch) {
        return (ch == '\n' || ch == '\r');
    }
}
